#!/usr/bin/env python3

import argparse
import apt_pkg
import json
import sys

cache = None
dep_cache = None
sr = None

_pkg_cache = set()
global_install_list = set()  # 保存全局待安装的软件包列表 (目标软件包 + 安装依赖链)
global_broken_list = set()  # 保存全局的Breaks和Conflicts关系


class NoPackage(Exception):
    def __init__(self, *args: object) -> None:
        super().__init__(*args)


class NoCandidatePackage(Exception):
    def __init__(self, *args: object) -> None:
        super().__init__(*args)


class NoInstalledPackage(Exception):
    def __init__(self, *args: object) -> None:
        super().__init__(*args)


class NoInstallablePackage(Exception):
    def __init__(self, *args: object) -> None:
        super().__init__(*args)


# 对输入的软件包进行虚包类型和无版本检测
def check_pkg_is_virtual_or_no_version(pkg: apt_pkg.Package) -> bool:
    """
    返回值: True表示检测到虚包或无版本的情况; 默认值为False，表示检测通过.

    下方关键属性解释：
    apt_pkg.Package.has_provides: Whether the package is provided by at least one other package.
    apt_pkg.Package.has_versions: Whether the package has at least one version in the cache.
    apt_pkg.Package.provides_list: A list of all packages providing this package. The list contains
                                    tuples in the format (providesname, providesver, version)
                                    where 'version' is an apt_pkg.Version object.
    """

    # 判断是虚包的情况
    if pkg.has_provides and not pkg.has_versions:
        true_package_list = set()  # 使用集合去重
        for provides in pkg.provides_list:
            _, _, _version = provides
            true_package_list.add(_version.parent_pkg.name)
        print(f'{pkg.name} 是虚包, 由 {" / ".join(true_package_list)} 提供')
        return True

    # 不是虚包，但是源里也没有任何版本
    elif not pkg.has_provides and not pkg.has_versions:
        print(f'{pkg.name} 不是虚包，但是源里也没有任何版本')
        return True

    return False


# 根据输入参数获取apt_pkg.Version对象
def get_pkg_version(pkg: apt_pkg.Package, ver_str: str, installed: bool) -> apt_pkg.Version:
    version = None
    # 指定版本号的情况
    if ver_str:
        # 查找对应的版本号
        for ver in pkg.version_list:
            if ver.ver_str == ver_str:
                version = ver
        # 检查是否有对应的版本号
        if version is None:
            raise NoPackage(f"No version {ver_str} for {pkg.name}")

    # 没有指定版本号的情况
    else:
        # 默认检查Candidate版本
        if installed is False:
            version = dep_cache.get_candidate_ver(pkg)
            if version is None:
                raise NoCandidatePackage(f"No candidate version for {pkg.name}")

        # 已声明选择检查已安装的版本
        else:
            version = pkg.current_ver
            if version is None:
                raise NoInstalledPackage(f"No installed version for {pkg.name}")

    return version


# 输出当前软件包的安装依赖
def print_pkg_depends(version: apt_pkg.Version):
    for deps in version.depends_list_str.get("PreDepends", []) + \
            version.depends_list_str.get("Depends", []):
        deps_str = []
        for dep in deps:
            # dep: 3-tuples of the form (name, version, operator)
            # operator is one of '<', '<=', '=', '>=', '>'.
            _name, _version, _operator = dep
            if _version or _operator:
                deps_str.append(f"{_name} ({_operator} {_version})")
            else:
                deps_str.append(f"{_name}")
        _show = " | ".join(deps_str)
        print(f"  Depends: {_show}")


# 输出当前软件包的冲突项 (Breaks & Conflicts)
def print_pkg_broken(version: apt_pkg.Version) -> set:
    broken_dep_set = set()  # 使用集合去重
    for broken_deps in version.depends_list_str.get("Conflicts", []) + \
                       version.depends_list_str.get("Breaks", []):
        for broken_dep in broken_deps:
            broken_dep_set.add(broken_dep)

    for broken_dep in broken_dep_set:
        _name, _version, _operator = broken_dep
        if _version or _operator:
            broken_dep_str = f"{_name} ({_operator} {_version})"
        else:
            broken_dep_str = f"{_name}"
        print(f"  Broken: {broken_dep_str}")

    return broken_dep_set


# 检查当前软件包是否与全局列表冲突列表是否有破坏性依赖关系
def check_pkg_with_global_broken_list(
        version: apt_pkg.Version, is_optional: bool, broken_dep_set: set) -> bool:
    for _name, _version, _operator in global_broken_list:
        if _name == version.parent_pkg.name:
            # 满足破坏性依赖项时，不能正常安装
            if apt_pkg.check_dep(version.ver_str, _operator, _version) is True:
                if is_optional is False:
                    raise NoCandidatePackage(
                        f"## Target package [{version.parent_pkg.name}/{version.ver_str}] "
                        f"not satisfy Breaks/Conflicts ({_operator} {_version})"
                    )
                else:
                    # 在全局列表冲突列表中，去掉刚才添加的Breaks/Conflicts项
                    global_broken_list.difference_update(broken_dep_set)
                    return False
    return True


# 根据依赖项包名获取apt_pkg.Package对象
def get_dep_pkg(package_name: str, is_optional: bool):
    try:
        dep_pkg = cache[package_name]
    except KeyError:
        if is_optional is True:
            return None
        else:
            raise NoPackage(f"## Not exists {package_name}")

    # 安装依赖是虚包的情况
    if dep_pkg.has_provides and not dep_pkg.has_versions:
        _, _, _version = dep_pkg.provides_list[0]
        dep_pkg = _version.parent_pkg

    return dep_pkg


# 检查当前软件包的Breaks和Conflicts字段是否与全局待安装列表冲突
def check_pkg_broken_with_global_install_list(
        version: apt_pkg.Version, is_optional: bool) -> bool:
    for broken_deps in version.depends_list.get("Conflicts", []) + \
                       version.depends_list.get("Breaks", []):
        for broken_dep in broken_deps:
            broken_package_name = broken_dep.target_pkg.name.split(":")[0]
            for name_version in global_install_list:
                if name_version[0] == broken_package_name:
                    # 满足破坏性依赖项时，不能正常安装
                    if apt_pkg.check_dep(name_version[1], broken_dep.comp_type, broken_dep.target_ver) is True:
                        if is_optional is False:
                            raise NoCandidatePackage(
                                f"## Candidate package [{name_version[0]}/{name_version[1]}] "
                                f"not satisfy Breaks/Conflicts ({broken_dep.comp_type} {broken_dep.target_ver})"
                            )
                        else:
                            return False
    return True


def check_depends(
    pkg: apt_pkg.Package,
    ver_str: str = "",
    installed: bool = False,
    is_optional: bool = False,
    check_broken: bool = False,
):
    """check dependencies by the given package

    Args:
        pkg (apt_pkg.Package): apt_pkg Package object
        ver_str (str, optional): package version str. Defaults to "".
        installed (bool, optional): True for check installed package, \
            False for check candidate version. Defaults to False.
        is_optional (bool, optional): whether the current package is optional. \
            If True and dependencies are unsatisfied, then raise Exception. Defaults to False.
        check_broken (bool, optional): whether the current package needs to be checked broken relationships. \
            If True and dependencies are unsatisfied, then raise Exception. Defaults to False.

    Raises:
        NoPackage: no package
        NoCandidatePackage: no candidate package
        print: show package info

    Returns:
        None: no return
        True: optional branch check passed
        False: optional branch check failed
    """

    # 当输入的包名有重复时, 防止重复执行和输出
    if pkg.name not in _pkg_cache:
        _pkg_cache.add(pkg.name)
    else:
        return

    # 输出当前包名
    print(f"{pkg.name}:")
    # 对输入的软件包进行虚包类型和无版本检测
    if check_pkg_is_virtual_or_no_version(pkg) is True:
        return
    # 根据输入参数获取apt_pkg.Version对象
    version = get_pkg_version(pkg, ver_str, installed)
    # 通过上一步基础检查后可以添加进全局安装列表
    global_install_list.add((pkg.name, version.ver_str))
    # 输出当前软件包的安装依赖
    print_pkg_depends(version)

    broken_dep_set = set()  # 使用集合去重
    if check_broken is True:
        # 输出当前软件包的冲突项 (Breaks & Conflicts)
        broken_dep_set = print_pkg_broken(version)
        # 将当前软件包的冲突项加入全局列表
        global_broken_list.update(broken_dep_set)
        # 检查当前软件包是否与全局列表冲突列表是否有破坏性依赖关系
        if check_pkg_with_global_broken_list(
                version, is_optional, broken_dep_set) is False:
            return False

    # is_optional变量可能在下方会被修改，缓存修改前的值
    is_optional_old = is_optional

    # 遍历安装依赖项, deps: list[apt_pkg.Dependency]
    for deps in version.depends_list.get("PreDepends", []) + \
            version.depends_list.get("Depends", []):
        branch_failed_count = 0
        optional_pkg_list = []
        # 判断此项安装依赖是否为多选一
        if len(deps) > 1:
            is_optional = True

        for dep in deps:
            package_name = dep.target_pkg.name.split(":")[0]
            if is_optional is True:
                optional_pkg_list.append(package_name)

            # 根据依赖项包名获取apt_pkg.Package对象
            dep_pkg = get_dep_pkg(package_name, is_optional)
            if dep_pkg is None and is_optional is True:
                branch_failed_count += 1
                continue

            # 获取候选版本
            try:
                candidate_pkg = dep_cache.get_candidate_ver(dep_pkg)
            except:
                raise NoCandidatePackage(f"## no candidate package for {dep_pkg.name}")

            if candidate_pkg is None:
                if is_optional is False:
                    raise NoCandidatePackage(f"## No candidate version for {dep_pkg.name}")
                else:
                    branch_failed_count += 1
                    continue

            # 检查候选版本是否满足安装依赖
            if apt_pkg.check_dep(candidate_pkg.ver_str, dep.comp_type, dep.target_ver) is False:
                if is_optional is False:
                    raise NoCandidatePackage(
                        f"## Candidate package [{candidate_pkg.parent_pkg.name}/{candidate_pkg.ver_str}] "
                        f"not satisfy requirements ({dep.comp_type} {dep.target_ver})"
                    )
                else:
                    branch_failed_count += 1
                    continue

            # 检查当前软件包的Breaks和Conflicts字段是否与全局待安装列表冲突
            if check_broken is True:
                check_result = check_pkg_broken_with_global_install_list(version, is_optional)
                if check_result is False and is_optional is True:
                    branch_failed_count += 1
                    continue

            # 递归调用，依次检查安装依赖链
            return_value = check_depends(pkg=dep.target_pkg, is_optional=is_optional, check_broken=check_broken)
            # print(f'[return_value] is {return_value}, {pkg.name} -> {dep.target_pkg.name}')  # 调试用

            # 传递某条依赖链不能正常安装的消息
            if return_value is False:
                # 不能安装的依赖链应该从全局包列表去掉
                _pkg_cache.discard(dep.target_pkg.name)
                # 在遍历集合时删除此集合内的元素时应该使用copy()方法，否则会报错：
                # RuntimeError: Set changed size during iteration
                for item in global_install_list.copy():
                    if item[0] == dep.target_pkg.name:
                        global_install_list.discard(item)

                # 传递某条依赖链不能正常安装的消息
                if is_optional_old is True:
                    # 在全局列表冲突列表中，去掉开头添加的Breaks/Conflicts项
                    if check_broken is True:
                        global_broken_list.difference_update(broken_dep_set)
                    return False
                else:
                    branch_failed_count += 1
                    continue

        # 将is_optional的值复位
        is_optional = is_optional_old

        # 当前可选依赖中的所有安装项全部都不满足依赖的情况
        if branch_failed_count == len(deps):
            if is_optional_old is True:
                return False
            else:
                raise NoInstallablePackage(f'## [{" | ".join(optional_pkg_list)}] are not installable')


def get_true_pkg(pkg: str) -> str:
    pkg_name = ""
    try:
        package = cache[pkg]
        # 虚包
        if package.has_provides and not package.has_versions:
            _, _, true_pkg = package.provides_list[0]
            pkg_name = true_pkg.parent_pkg.name
        else:
            pkg_name = pkg
    finally:
        return pkg_name


def get_recode(pkg) -> bool:
    sr.restart()
    return sr.lookup(pkg)


pkg_cache = []


def get_source_dependencies(pkg: str, ver_str: str = "", result: dict = {}, *keys) -> dict:
    pkg = get_true_pkg(pkg=pkg)
    if not pkg:
        return {pkg: {"source": "", "version": ""}}
    if pkg not in pkg_cache:
        pkg_cache.append(pkg)
        print(pkg)
    else:
        return {}
    '''
    {
        "pkg_name": {
            "required": {
                "comp": ">",
                "version": "1.0.0"
            },
            "source": "",
            "version": "",
        }
    }
    '''
    keys = ["Build-Depends", "Build-Depends-Indep"]
    if not get_recode(pkg):
        return {pkg: {"source": "", "version": ""}}

    build_depends = sr.build_depends
    if not build_depends:
        return {}

    for key in keys:
        for deps in build_depends.get(key, []):
            for dep in deps:
                # dep: pkg_name version comp
                # print(dep)
                pkg_name, version, comp = dep
                pkg_name = get_true_pkg(pkg=pkg_name)
                if not pkg_name or result.get(pkg_name):
                    continue

                if not get_recode(pkg_name):
                    return {}
                try:
                    _r = {pkg_name: {"required": {"comp": comp, "version": version}, "source": sr.package, "version": sr.version}}
                except Exception as e:
                    print(pkg_name)
                    raise e

                result.update(_r)
                result.update(get_source_dependencies(pkg=pkg_name, result=result))

    return result


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-b",
        "--build-depends",
        dest="build_depends",
        action="store_true",
        default=False,
        help="获取编译依赖链",
    )
    # 将输入的包名自动转为字符串列表
    parser.add_argument(
        "--package",
        dest='package',
        nargs="+",
        type=str,
        default=[],
        help="二进制包名称",
    )
    # 以下两个参数应该设置为互斥关系，不可同时输入
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        "-e",
        "--version",
        dest="version",
        default="",
        help="软件包的版本号，如果为空会根据installed参数选择已安装版本或者是candidate版本",
    )
    group.add_argument(
        "-i",
        "--installed",
        dest="installed",
        action="store_true",
        default=False,
        help="检查已安装的版本",
    )

    args = parser.parse_args()

    apt_pkg.init()
    cache = apt_pkg.Cache()  # 加载本地apt缓存文件
    dep_cache = apt_pkg.DepCache(cache=cache)

    if args.build_depends:
        sr = apt_pkg.SourceRecords()
        result = {}
        for search_package in args.package:
            result = get_source_dependencies(search_package)
        with open(f"test.json", 'w') as f:
            json.dump(result, f, indent=2)
        sys.exit(0)

    # 单包检查
    if args.package:
        for search_package in args.package:
            # 判断输入的(二进制包)包名是否在本地缓存的包列表中
            if search_package in cache:
                package = cache[search_package]
                check_depends(package, args.version, args.installed,
                              check_broken=True)
            else:
                print(f"## no package named by {search_package}")
                sys.exit(1)
    # 整个仓库的所有软件包检查
    else:
        for package in cache.packages:
            # check_depends(package, check_broken=True)
            try:
                check_depends(package, check_broken=True)
            except Exception as e:
                print(f"[{package.name}] failure with: {e}")
                sys.exit(1)

    # 调试用
    # print(len(_pkg_cache), len(global_install_list), len(global_broken_list))
    # for i in global_broken_list:
    #     print(type(i), i)
